package com.bvbot.core.bot;

import static com.bvbot.core.Logger.log;

import java.math.BigDecimal;
import java.util.Map;
import java.util.Map.Entry;

import com.bvbot.core.BvBotRuntimeException;
import com.bvbot.core.session.BalanceResponse;
import com.bvbot.core.session.BvSession;
import com.bvbot.core.session.BvSessionException;
import com.bvbot.core.session.MarketResponse;

/**
 * Sell if price drops under a limit or price drops too quick.
 * 
 * @author otexier
 * 
 */
public class VolatilityBot extends AbstractBot<VolatilityBotConfig> {

	private Float allTimeMaxSecurityPrice;
	private Float allTimeMinSecurityPrice;
	private BigDecimal lastBuyPrice;
	private BigDecimal lastSellPrice;

	private enum CurrentState {
		GRAMS_NO_CASH, CASH_NO_GRAMS
	}

	private CurrentState currentState = null;

	public VolatilityBot(VolatilityBotConfig config) {
		super(config);
	}

	@Override
	public Decision computeDecision() {
		if (getCurrentState() == null) {
			computeAndSetCurrentState();
		}
		Prices pNow = getHistory().getCurrentPrice();
		if (pNow == null || pNow.getGoldWorldValue() == null) {
			log("Impossible to compute a decision : no current price");
			return Decision.NOTHING;
		}
		if (allTimeMaxSecurityPrice == null) {
			allTimeMaxSecurityPrice = pNow.getGoldWorldValue();
		} else {
			if (allTimeMaxSecurityPrice < pNow.getGoldWorldValue()) {
				allTimeMaxSecurityPrice = pNow.getGoldWorldValue();
			}
		}
		if (allTimeMinSecurityPrice == null) {
			allTimeMinSecurityPrice = pNow.getGoldWorldValue();
		} else {
			if (allTimeMinSecurityPrice > pNow.getGoldWorldValue()) {
				allTimeMinSecurityPrice = pNow.getGoldWorldValue();
			}
		}
		// max decrease check
		Float cd = (allTimeMaxSecurityPrice - pNow.getGoldWorldValue());
		if (cd >= getConfig().getMaxChangeLimit()) {
			log("Current decrease " + cd + " is below the limit "
					+ getConfig().getMaxChangeLimit() + ". Decision to sell");
			return Decision.SELL;
		}
		// max increase check
		Float ci = (pNow.getGoldWorldValue() - allTimeMinSecurityPrice);
		if (ci >= getConfig().getMaxChangeLimit()) {
			log("Current increase " + ci + " is above the limit "
					+ getConfig().getMaxChangeLimit() + ". Decision to buy");
			return Decision.BUY;
		}
		// real prices log
		logRealPrices();
		// changes by minutes check
		Decision changesByMinutesDecision = Decision.NOTHING;
		Map<Integer, Float> changesByMinutes = getConfig()
				.getMaxChangeByMinutes();
		if (changesByMinutes != null && !changesByMinutes.isEmpty()) {
			for (Entry<Integer, Float> entry : changesByMinutes.entrySet()) {
				changesByMinutesDecision = computeDecision(entry,
						getCurrentState(), pNow);
				if (!(changesByMinutesDecision == Decision.NOTHING)) {
					return changesByMinutesDecision;
				}
			}
		}

		log("Current world price is : " + pNow.getGoldWorldValue()
				+ ". All time max price is " + allTimeMaxSecurityPrice
				+ ". Current decrease between max and now is " + cd
				+ ". All time min price is " + allTimeMinSecurityPrice
				+ ". Current increase between min and now is " + ci + ".");
		return Decision.NOTHING;
	}

	private void logRealPrices() {
		BvSession session = new BvSession();
		MarketResponse mr = null;
		String securityDescription = null;
		try {
			if (getConfig().isGold()) {
				mr = session.getGoldMarket();
				securityDescription = "Gold in Zurich";
			} else {
				mr = session.getSilverMarket();
				securityDescription = "Silver in London";
			}
			BigDecimal marketPrice = mr.getMarketPrice();
			BigDecimal bestBuyPrice = mr.computeHighestBuyPrice();
			BigDecimal bestSellPrice = mr.computeLowestSellPrice();
			log(securityDescription + " : marketPrice = " + marketPrice
					+ ". Best price to buy security : " + bestSellPrice
					+ ". Best price to sell security : " + bestBuyPrice);
		} catch (BvSessionException e) {
			log("Impossible to logRealPrices : " + e.getMessage(), e);
		}
	}

	private CurrentState computeAndSetCurrentState() {
		CurrentState result = null;
		BvSession session = new BvSession();
		try {
			session.start();
			BalanceResponse balance = session.getBalance();
			int securityGrams = 0;
			if (getConfig().isGold()) {
				securityGrams = balance.getGoldGrams();
			} else {
				securityGrams = balance.getSilverGrams();
			}
			if (securityGrams != 0) {
				result = CurrentState.GRAMS_NO_CASH;
				log("They are " + securityGrams + " grams left. Bot goes in "
						+ CurrentState.GRAMS_NO_CASH.name() + " mode.");
			} else {
				result = CurrentState.CASH_NO_GRAMS;
				log("They are no grams left. Bot goes in "
						+ CurrentState.CASH_NO_GRAMS.name() + " mode.");
			}
		} catch (BvSessionException e) {
			log("Exception reçue : " + e.getMessage(), e);
		}
		setCurrentState(result);
		return result;
	}

	private Decision computeDecision(Entry<Integer, Float> entry,
			CurrentState currentState, Prices currentPrice) {
		Decision result = Decision.NOTHING;
		if (entry != null && currentState != null) {
			Prices extremePrice = null;
			Float change = null;
			switch (currentState) {
			case GRAMS_NO_CASH:
				extremePrice = getHistory().getMaxPriceBeforeNowMinutes(
						entry.getKey());
				if (extremePrice == null) {
					extremePrice = currentPrice;
				}
				change = extremePrice.getGoldWorldValue()
						- currentPrice.getGoldWorldValue();
				log("" + currentState.name() + " mode : Change in "
						+ entry.getKey() + " minutes is " + change);
				if (change > entry.getValue()) {
					log("" + currentState.name() + " mode : Change in "
							+ entry.getKey() + " minutes is greater than "
							+ entry.getValue() + " limit : " + change
							+ ". Decision to SELL");
					result = Decision.SELL;
				}
				break;
			case CASH_NO_GRAMS:
				extremePrice = getHistory().getMinPriceBeforeNowMinutes(
						entry.getKey());
				if (extremePrice == null) {
					extremePrice = currentPrice;
				}
				change = currentPrice.getGoldWorldValue()
						- extremePrice.getGoldWorldValue();
				log("" + currentState.name() + " mode : Change in "
						+ entry.getKey() + " minutes is " + change);
				if (change > entry.getValue()) {
					log("" + currentState.name() + " mode : Change in "
							+ entry.getKey() + " minutes is greater than "
							+ entry.getValue() + " limit : " + change
							+ ". Decision to BUY");
					result = Decision.BUY;
				}
				break;
			default:
				throw new BvBotRuntimeException(
						"Unexpected : currentState unknown : " + currentState);
			}
		}
		return result;
	}

	@Override
	protected void beforeRun() {
		log("Start of " + this + " with config  " + getConfig());
	}

	@Override
	protected void applyDecision(Decision decision) {
		switch (decision) {
		case SELL:
			log("Decision to sell !");
			logMarketPrice(true);
			if (getConfig().isGold()) {
				sellGold();
			} else {
				sellSilver();
			}
			reinitState();
			break;
		case BUY:
			log("Decision to buy !");
			logMarketPrice(false);
			if (getConfig().isGold()) {
				buyGold();
			} else {
				buySilver();
			}
			reinitState();
			break;
		default:
			break;
		}
	}

	private void logMarketPrice(boolean isSale) {
		try {
			BvSession sess = new BvSession();
			MarketResponse mr = null;
			if (getConfig().isGold()) {
				mr = sess.getGoldMarket();
			} else {
				mr = sess.getSilverMarket();
			}
			if (isSale) {
				lastSellPrice = mr.computeHighestBuyPrice();
			} else {
				lastBuyPrice = mr.computeLowestSellPrice();
			}
			if (isSale && lastBuyPrice != null && lastSellPrice != null) {
				log("MarketPrice : sell will be done at " + lastSellPrice);
				log("Benefit between last buy and sell is "
						+ lastSellPrice.subtract(lastBuyPrice) + ".");
			} else {
				log("MarketPrice : buy will be done at " + lastBuyPrice);
			}
		} catch (BvSessionException e) {
			log("Exception in logMarketPrice : " + e.getMessage(), e);
		}
	}

	@Override
	protected void reinitState() {
		setCurrentState(null);
		allTimeMaxSecurityPrice = null;
		allTimeMinSecurityPrice = null;
		getHistory().clearHistory();
	}

	private CurrentState getCurrentState() {
		return currentState;
	}

	private void setCurrentState(CurrentState cs) {
		this.currentState = cs;
	}

}
